package com.myc.train.business.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.myc.train.business.domain.*;
import com.myc.train.business.enums.ConfirmOrderStatusEnum;
import com.myc.train.business.enums.SeatColEnum;
import com.myc.train.business.enums.SeatTypeEnum;
import com.myc.train.business.req.ConfirmOrderTicketReq;
import com.myc.train.common.context.LoginMemberContext;
import com.myc.train.common.exception.BusinessException;
import com.myc.train.common.exception.BusinessExceptionEnum;
import com.myc.train.common.resp.PageResp;
import com.myc.train.common.utils.SnowUtil;
import com.myc.train.business.mapper.ConfirmOrderMapper;
import com.myc.train.business.req.ConfirmOrderQueryReq;
import com.myc.train.business.req.ConfirmOrderDoReq;
import com.myc.train.business.resp.ConfirmOrderQueryResp;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class ConfirmOrderService {

    private static final Logger LOG = LoggerFactory.getLogger(ConfirmOrderService.class);
    @Resource
    private ConfirmOrderMapper confirmOrderMapper;

    @Resource
    private DailyTrainTicketService dailyTrainTicketService;

    @Resource
    private DailyTrainCarriageService dailyTrainCarriageService;

    @Resource
    private DailyTrainSeatService dailyTrainSeatService;
    @Resource
    private AfterConfirmOrderService afterConfirmOrderService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

//    @Autowired
//    private RedissonClient redissonClient;
    public void save(ConfirmOrderDoReq req){
        DateTime now = DateTime.now();
        ConfirmOrder confirmOrder = BeanUtil.copyProperties(req, ConfirmOrder.class);

        if(ObjectUtil.isNull(confirmOrder.getId())){
            confirmOrder.setId(SnowUtil.getSnowflakeNextId());
            confirmOrder.setCreateTime(now);
            confirmOrder.setUpdateTime(now);
            confirmOrderMapper.insert(confirmOrder);
        }else{
            confirmOrder.setUpdateTime(now);
            confirmOrderMapper.updateByPrimaryKey(confirmOrder);
        }
    }

    public PageResp<ConfirmOrderQueryResp> queryList(ConfirmOrderQueryReq req){
       //定义查询条件
        ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
        //根据id倒序排序
        confirmOrderExample.setOrderByClause("id desc");
        //创建查询对象
        ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
        //不为空则对其新增查询条件


        //新增分页信息
        PageHelper.startPage(req.getPage(),req.getSize());
        //根据条件查询结果
        List<ConfirmOrder> confirmOrders = confirmOrderMapper.selectByExample(confirmOrderExample);
        //new一个PageInfo并将结果放进来
        PageInfo<ConfirmOrder> confirmOrderPageInfo = new PageInfo<>(confirmOrders);
        //类拷贝
        List<ConfirmOrderQueryResp> queryResps = BeanUtil.copyToList(confirmOrders, ConfirmOrderQueryResp.class);
        //new一个PageResp
        PageResp<ConfirmOrderQueryResp> resp= new PageResp<>();
        //放数据
        resp.setTotal(confirmOrderPageInfo.getTotal());
        resp.setList(queryResps);


        return resp;
    }

    public void delete(Long id){
        confirmOrderMapper.deleteByPrimaryKey(id);
    }



    //下单接口开发
    public  void doConfirm(ConfirmOrderDoReq req) {
        //定义key
        String lockkey = DateUtil.formatDate(req.getDate()) + "-" + req.getTrainCode();
        //添加redis分布式锁
        Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockkey, 20, TimeUnit.SECONDS);
        //判断是否拿到锁
        if (setIfAbsent){
            LOG.info("拿到锁了，往下执行操作");
        }else {
            LOG.info("没拿到锁，不往下执行");
            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_ERROR);
        }
            //0.业务校验（省略不写）：例如：车次是否存在，余票是否存在，车次是否在有效期内，tickets条数》0，同乘客同车次是否重复购买等

        try{


            //1 保存确认订单表，状态为初始状态
                DateTime now = DateTime.now();
                Date date = req.getDate();
                String trainCode = req.getTrainCode();
                String start = req.getStart();
                String end = req.getEnd();
                List<ConfirmOrderTicketReq> tickets = req.getTickets();

                ConfirmOrder confirmOrder = new ConfirmOrder();
                confirmOrder.setId(SnowUtil.getSnowflakeNextId());
                confirmOrder.setMemberId(LoginMemberContext.getId());
                confirmOrder.setDate(date);
                confirmOrder.setTrainCode(trainCode);
                confirmOrder.setStart(start);
                confirmOrder.setEnd(end);
                confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
                confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
                confirmOrder.setCreateTime(now);
                confirmOrder.setUpdateTime(now);
                confirmOrder.setTickets(JSON.toJSONString(tickets));
                confirmOrderMapper.insert(confirmOrder);
                //2 查出余票数量，需要得到真实的库存
                DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);
                LOG.info("查出余票数量: {}", dailyTrainTicket);

                //3 扣减余票数量，并判断余票是否足够
                reduceTickets(req, dailyTrainTicket);

                //最终的选择结果
                List<DailyTrainSeat> finalSeatList = new ArrayList<>();

                //4.计算相对座位偏移值： 例如：c1 ,d2 :[0,5], a1.b1,c1:[0,1,2]
                //判断是否可以选座--seat为null则是无法选座，选到列表的第一个座位
                ConfirmOrderTicketReq ticketReq0 = tickets.get(0);
                if (StrUtil.isNotBlank(ticketReq0.getSeat())) {
                    LOG.info("可以选座");
                    //查出本次选座的座位类型，是一等座还是二等座
                    List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode());
                    LOG.info("本次选座的座位类型包含的列：{}", colEnumList);

                    //组装和前台选座一样的两排座位：例如【a1,b1,c1,d1,a2.b2.c2.d2】
                    //定义一个装座位的列表
                    List<String> referSeatList = new ArrayList<>();
                    //循环插入值
                    for (int i = 1; i <= 2; i++) {
                        for (SeatColEnum seatColEnum : colEnumList) {
                            referSeatList.add(seatColEnum.getCode() + i);
                        }
                    }
                    LOG.info("选座的两排座位是：{}", referSeatList);

                    //计算座位的偏移值，先获取绝对偏移值--》1-6；之后通过第一个座位后进行排可得到所有的相对偏移
                    //先把所有票的结对偏移值进行存储
                    List<Integer> aboluteOffsetList = new ArrayList<>();
                    for (ConfirmOrderTicketReq ticketReq : tickets) {
                        //ticketReq.getSeat()在referSeatList这个参照系中的位置就是偏移值
                        int index = referSeatList.indexOf(ticketReq.getSeat());
                        aboluteOffsetList.add(index);
                    }
                    LOG.info("计算得到所有座位的偏移值：{}", aboluteOffsetList);

                    //计算相对偏移值.相对于第一个座位的偏移值
                    List<Integer> offsetList = new ArrayList<>();
                    for (Integer index : aboluteOffsetList) {
                        int offset = index - aboluteOffsetList.get(0);
                        offsetList.add(offset);
                    }
                    LOG.info("计算得到所有的相对第一个座位的偏移值：{}", offsetList);

                    //选座--拿到第一张票的类型信息，因为选座都是一样的座位
                    getSeat(finalSeatList,
                            date,
                            trainCode,
                            ticketReq0.getSeatTypeCode(),//枚举值的类型
                            ticketReq0.getSeat().split("")[0],//获取座位类型，将取到的A1，通过split分割成【A,1】取第一项也就是A
                            offsetList,
                            dailyTrainTicket.getStartIndex(),
                            dailyTrainTicket.getEndIndex());

                } else {
                    LOG.info("无法选座");
                    //选座，因为车厢可能不同，所有车票需要分开查看类型如何，系统分配座位
                    for (ConfirmOrderTicketReq ticketReq : tickets) {
                        getSeat(finalSeatList, date, trainCode, ticketReq.getSeatTypeCode(),
                                null, null,
                                dailyTrainTicket.getStartIndex(),
                                dailyTrainTicket.getEndIndex());
                    }
                }

                LOG.info("最终选座：{}", finalSeatList);

                //选中座位后进行事务处理
                try {
                    afterConfirmOrderService.afterDoConfirm(dailyTrainTicket, finalSeatList, tickets, confirmOrder);
                } catch (Exception e) {
                    LOG.error("报错购票信息失败：{}", e);
                    throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_EXCEPTION);
                }
        }finally {
            LOG.info("操作完毕，释放锁：{}",lockkey);
            stringRedisTemplate.delete(lockkey);
        }
    }

    //挑选座位
    private void getSeat(List<DailyTrainSeat> finalSeatList,Date date, String trainCode, String seatType, String column, List<Integer> offsetList,
                         Integer startIndex, Integer endIndex){
        //声明临时的选座列表
        List<DailyTrainSeat> getSeatList = new ArrayList<>();
        //获取符合条件的车厢列表
        List<DailyTrainCarriage> carriageList = dailyTrainCarriageService.selectBySeatType(date, trainCode, seatType);
        LOG.info("查询到符合条件的车厢：{}个",carriageList.size());
        //挨个车厢获取座位数
        for(DailyTrainCarriage dailyTrainCarriage:carriageList){
            LOG.info("开始从车厢{}选座",dailyTrainCarriage.getIndex());
            //换车厢则清空临时的选座列表
            getSeatList = new ArrayList<>();
            //根据车厢查出座位列表
            List<DailyTrainSeat> seatList = dailyTrainSeatService.selectByCarriage(date, trainCode, dailyTrainCarriage.getIndex());
            LOG.info("车厢{}的座位数：{}",dailyTrainCarriage.getIndex(),seatList.size());
            //按照车厢查找合适的座位
            for (int i = 0; i < seatList.size(); i++) {
                DailyTrainSeat dailyTrainSeat = seatList.get(i);
                //获取座位数据
                String col = dailyTrainSeat.getCol();
                Integer seatIndex = dailyTrainSeat.getCarriageSeatIndex();
                //判断当前座位不能被选中
                boolean alreadyChooseFlag = false;
                for (DailyTrainSeat finalSeat: finalSeatList) {
                    if (finalSeat.getId().equals(dailyTrainSeat.getId())){
                        alreadyChooseFlag = true;
                        break;
                    }

                }
                if(alreadyChooseFlag){
                    LOG.info("座位{}被选中过，不能重复选择，继续判断下个座位",seatIndex);
                    continue;
                }

                //判断column，若存在则比对列号是否相同
                if (StrUtil.isBlank(column)) {
                    LOG.info("无选座");
                } else {
                    if (!column.equals(col)) {
                        LOG.info("座位{}列值不对，继续判断下一个座位，当前列值：{}，目标列值：{}", seatIndex, col, column);
                        continue;
                    }
                }

                //判断是否选中了座位
                boolean isChoose = calSell(dailyTrainSeat, startIndex, endIndex);
                if (isChoose) {
                    getSeatList.add(dailyTrainSeat);
                    LOG.info("选中了座位");

                } else {
                    continue;
                }

                //根据offset选择剩下的座位
                boolean isGetAllOffsetSeat = true;
                if (CollUtil.isNotEmpty(offsetList)) {
                    LOG.info("有偏移值：{},校验偏移的座位是否可选", offsetList);
                    //1.找到偏移值的座位,从索引1开始，索引0是当前以选择的票
                    for (int j = 1; j < offsetList.size(); j++) {
                        //2.计算是否可选
                        //拿到偏移值
                        Integer offset = offsetList.get(j);
                        //计算相对偏移值
                        int nextIndex = i + offset;
                        //有选座时，一段要在同一个车厢
                        if (nextIndex >= seatList.size()) {
                            LOG.info("座位{}不可选，偏移后的索引超出了这个车箱的座位数", nextIndex);
                            isGetAllOffsetSeat = false;
                            break;
                        }

                        //看看当前偏移值的座位是否可选
                        DailyTrainSeat nextDailyTrainSeat = seatList.get(nextIndex);
                        //判断是否选中了座位
                        Boolean isChooseNext = calSell(nextDailyTrainSeat, startIndex, endIndex);
                        if (isChooseNext) {
                            LOG.info("座位{}被选中", nextDailyTrainSeat.getCarriageSeatIndex());
                            getSeatList.add(nextDailyTrainSeat);
                        } else {
                            LOG.info("座位{}不可选", nextDailyTrainSeat.getCarriageSeatIndex());
                            isGetAllOffsetSeat = false;
                            break;
                        }
                    }


                }
                if (!isGetAllOffsetSeat) {
                   //没有选中则清空临时的选座列表
                    getSeatList = new ArrayList<>();
                    continue;
                }

                //保存选好的座位
                finalSeatList.addAll(getSeatList);
                return;
            }
        }

    }

    /**
     *  计算某座位在区间内是否可卖
     *  具体逻辑如下：例：sell=10001，1代表已卖出，0代表未卖出
     *  买票选中后，计算sell，加入本次买1-4站
     *  实现过程：通过构造本次买票生成的售卖信息01110，和原来的sell信息进行或运算，也就是01110和10001按位或运算，得到11111
     */

    private Boolean calSell(DailyTrainSeat dailyTrainSeat, Integer startIndex, Integer endIndex){
        //获取座位的售卖信息--->例如：10101
        String sell = dailyTrainSeat.getSell();
        //截取购买车票的区间--->例如 010
        String sellPart = sell.substring(startIndex, endIndex);
        //将其进行判断，转换为数字，若是其大于0证明区间内有卖出，则该作为无法售卖，为0则都没卖，000
        if(Integer.parseInt(sellPart)>0){
            LOG.info("座位{}在本次车站区间{}--{}已卖过票，不可选中座位",dailyTrainSeat.getCarriageSeatIndex(),startIndex,endIndex);
            return false;
        }else {
            LOG.info("座位{}在本次车站区间{}--{}未卖过票，可以选中座位",dailyTrainSeat.getCarriageSeatIndex(),startIndex,endIndex);
            //选中座位，首先将选中的区间把0变为1.'000'--->'111'
            String curSell = sellPart.replace('0', '1');
            //将前面补0，例如：买第3到八站，区间为 00011111，目前截取的区间为11111，所以需补3个0，通过endIndex可知长度
            curSell = StrUtil.fillBefore(curSell, '0', endIndex);
            //将后面也要补0，00011111---》00011111000
            curSell = StrUtil.fillAfter(curSell,'0',sell.length());
            //将区间售卖信息 culSell 01110与库里的已售卖信息sell 00001进行按位或运算，即可得到新座位
            //将字符串转成二进制进行或运算,得到一个数字，例如得到了14
            int newSellInt = NumberUtil.binaryToInt(curSell) | NumberUtil.binaryToInt(sell);
            //将数字在转回二进制字符串
            String newSell = NumberUtil.getBinaryStr(newSellInt);
            //注意，转回的二进制长度不一定狗，需要在前方进行补0
            newSell = StrUtil.fillBefore(newSell , '0', sell.length());
            LOG.info("座位{}被选中，原售卖信息：{}，车站区间：{}---{}，即：{}，最终售票信息：{}",
                    dailyTrainSeat.getCarriageSeatIndex(),sell,startIndex,endIndex,endIndex,
                    curSell,newSell);
            //将售卖信息更新到dailyTrainSeat的sell中
            dailyTrainSeat.setSell(newSell);
            return true;
        }

    }

    //扣减余票数量
    private static void reduceTickets(ConfirmOrderDoReq req, DailyTrainTicket dailyTrainTicket) {
        for (ConfirmOrderTicketReq ticketReq: req.getTickets()) {
            //获取座位码
            String seatTypeCode = ticketReq.getSeatTypeCode();
            //获取座位对应的枚举值
            SeatTypeEnum seatTypeEnum = EnumUtil.getBy(SeatTypeEnum::getCode, seatTypeCode);
            //循环判断
            switch (seatTypeEnum){
                case YDZ -> {
                    int countLeft = dailyTrainTicket.getYdz() - 1;
                    if(countLeft<0){
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setYdz(countLeft);
                }
                case EDZ -> {
                    int countLeft = dailyTrainTicket.getEdz() - 1;
                    if(countLeft<0){
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setEdz(countLeft);
                }
                case RW -> {
                    int countLeft = dailyTrainTicket.getRw() - 1;
                    if(countLeft<0){
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setRw(countLeft);
                }
                case YW -> {
                    int countLeft = dailyTrainTicket.getYw() - 1;
                    if(countLeft<0){
                        throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR);
                    }
                    dailyTrainTicket.setYw(countLeft);
                }

            }

        }
    }

}
